/**
 * 
 */
package com.google.code.netz.wfe.store;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.code.netz.wfe.api.EntityNotFoundException;
import com.google.code.netz.wfe.model.IPetriNetEntity;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;

/**
 * @author <a href='mailto:rahul.thakur.xdev@gmail.com'>Rahul Thakur</a>
 * @version $Id: StoreService.java 17 2009-02-09 13:08:49Z rahul.thakur.xdev $
 * @since 1.0
 */
public class StoreService {

	private static final Log log = LogFactory.getLog(StoreService.class);

	@Inject
	private static Injector injector;

	private static final Object mutex = new Object();

	/**
	 * Cached {@link Store} instances keyed on Entity Class.
	 */
	private static Map<Class<? extends IPetriNetEntity>, Provider<?>> cache = new HashMap<Class<? extends IPetriNetEntity>, Provider<?>>();

	/**
	 * 
	 * @param <T>
	 * @param klass
	 * @param id
	 * @return
	 * @throws EntityNotFoundException
	 * @throws StoreException
	 */
	public static <T extends IPetriNetEntity> T lookup(Class<T> klass, Long id) throws EntityNotFoundException,
			StoreException {
		return getStoreInstance(klass).lookup(klass, id);
	}

	/**
	 * 
	 * @param <T>
	 * @param klass
	 * @param entity
	 * @return
	 * @throws StoreException
	 */
	public static <T extends IPetriNetEntity> T save(Class<T> klass, T entity) throws StoreException {
		return getStoreInstance(klass).save(entity);
	}

	/**
	 * 
	 * @param <T>
	 * @param klass
	 * @param entity
	 * @throws StoreException
	 */
	public static <T extends IPetriNetEntity> void delete(Class<T> klass, T entity) throws StoreException {
		getStoreInstance(klass).delete(entity);
	}

	public static <T extends IPetriNetEntity> List<T> query(Class<T> klass, Query<T> query, int start, int range)
			throws StoreException {
		return getStoreInstance(klass).query(query, start, range);
	}

	/**
	 * 
	 * @param <T>
	 * @param entityKlass
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private static <T extends IPetriNetEntity> Store<T, Query<T>> getStoreInstance(final Class<T> entityKlass) {
		long startTime = System.nanoTime();

		try {
			final ParameterizedType parameterizedQueryType = new ParameterizedType() {

				public Type[] getActualTypeArguments() {
					return new Type[] { entityKlass };
				}

				public Type getOwnerType() {
					return null;
				}

				public Type getRawType() {
					return Query.class;
				}
			};

			ParameterizedType parameterizedStoreType = new ParameterizedType() {

				public Type[] getActualTypeArguments() {
					// Here we tell the runtime that one of these Type arguments
					// is
					// another ParameterizedType. Example: Store<Project,
					// Query<Project>>
					return new Type[] { entityKlass, parameterizedQueryType };
				}

				public Type getOwnerType() {
					return null;
				}

				public Type getRawType() {
					return Store.class;
				}
			};

			TypeLiteral<?> typeLiteral = TypeLiteral.get(parameterizedStoreType);

			// Check if there is a Store Provider available
			if (null == cache.get(entityKlass)) {
				if (injector.findBindingsByType(typeLiteral).size() > 0) {
					Provider<?> provider = injector.findBindingsByType(typeLiteral).get(0).getProvider();
					synchronized (mutex) {
						// again check if there is a Store provider still not
						// available
						if (null == cache.get(entityKlass))
							cache.put(entityKlass, provider);
					}
				}
			}

			Store<T, Query<T>> store = (Store<T, Query<T>>) cache.get(entityKlass).get();

			return store;

		} finally {
			long endTime = System.nanoTime();
			log.debug(String.format("%s nanoseconds taken to obtain Store Provider instance for %s entity",
					(endTime - startTime), entityKlass.getSimpleName()));
		}
	}
}
